Websydian v6.1 online documentationOnline documentation - WebsydianExpress v3.5

Recommendation for writing solid code

Introduction

Developers can perform a number of tasks to avoid or detect errors in the Plex production environment. Checking *Call status and *Returned status is not difficult but it can be time consuming, tedious and difficult to perform systematically. And what do you do when a function call returns *Instance not found?

In WebsydianExpress we have created a Catch feature which facilitates writing solid code for Plex applications. The purpose is to systematically detect, handle and report errors in business processes before they reach the production environment - and in case they should have gone undetected into production.

The method is in no way compulsory. It is meant as an inspiration for creating code where unexpected errors are easier to detect and investigate. We recommend that you use the parts you find relevant - and to further develop the method yourself according to your specific needs.

You may already have solved the issues that this document addresses, but there may still be a couple of pointers you can use.

In our experience, generally two factors are most likely to cause problems in the production environment:

  1. Uninitialized local and output fields
  2. Missing check for errors on function calls

Of course there are a lot of other reasons for errors, but these two seem to occur more often in production environments - and they can often be very hard to identify as the errors often only occurs in very special situations.

Initializing fields

The abstract WebsydianExpress functions contain meta-code that initializes all local and global fields (see for instance Abstract.FunctionShell, Pre Point Start initialize). At this point the Environment<*Returning status> is also set to *Successful.

When you create functions that do not inherit from these abstract functions, we strongly recommend that you create an abstract function that clears these variables - and then let all your functions inherit from the abstract function you created.

However, you must consider carefully whether this is feasible before clearing the variables as there are a few functions that depend on the fact that the values of the fields are maintained between calls.

Checking for errors

Checking *Returned Status

Most developers are very aware that they should check the Environment<*Returned Status> after a program call. Still, some times this is not done.

Checking *Call status

While the returned status in most cases is handled correctly, the situation is quite different when it comes to the call status.

An abnormal call status usually indicates a non-existing function object. In most of the models we have seen, the call status are not checked at all - or only very rarely.

The problem of not checking the call status is that the returned status of a call to a non-existing program actually is successful. So if you only check the returned status, the calling program will continue as after a successful call.

This means that to solve the error-checking problem fully, you must check both the returned status and the call status - and the program must take a default action if unhandled errors occur.

The Catch subroutine

An easy way to help in solving the error checking issue, the abstract WebsydianExpress functions contain a subroutine called "Catch".

This subroutine checks both the call status and the returned status.

If the call status isn't *Normal or the returned status isn't successful, an error is written to the message log, a text message is generated for the end user, and the program terminates with the *Returning Status = SignalTerminate (ERROR).

The message that is written to the message log contains the identification of the calling function (the current function) - and the called function. The called function is taken from the field Environment<*Object> - which means that you have to populate this field before using the Catch subroutine.

Using the Catch subroutine

The aim is to ensure that all unexpected errors are registered in the message log and that processing is terminated when an unexpected error occurs.

Calling functions that should never return errors

A very common case is that you have a function that should never return an error.

Use the Catch subroutine as follows:

 

Name: Function: MyFunction, Environment<*Object>, Scoped

Call MyFunction

Go Sub Catch

 

If at any time MyFunction is not available (e.g. when deploying in the production environment), the Catch subroutine will check the call status - and report an error in the message log. As you have populated the field Environment<*Object> with the name of MyFunction, the message will contain both name of the calling and the called function (the name of the calling function is resolved by meta-code).

The assumption is that MyFunction always return successful in the returned status. If at any time this does not happen, it is recorded in the message log.

The Catch subroutine checks both status fields and reports any errors to the message log. At the same time, the calling function terminates and returns a special error code. The function also generates a message for the end user and places it on the message stack

Whether this message will be shown depends on the handling of the calling programs in the call hierarchy. If the calling programs do not check for errors and at some point show an error page, the error will never be shown to the user.

Calling a function where you want to accept any error

In some cases, you are willing to accept any error from a function (e.g. if you are just retrieving some informational data that are not vital for the further processing).

Even though you are willing to accept any error, you should not accept a missing program - so you should still check the call status.

You can perform this check like this:

 

Name: Function: MyFunction, Environment<*Object>, Scoped

Call MyFunction

If Environment<*Call status> != <*Call status.*Normal>

    Go Sub Catch

 

If the call status is abnormal, the Catch routine writes the error to the message log.

Calling a function that can return acceptable errors

Many functions can return a non-successful return status without it being an error.

The most obvious example is a SingleFetch function.

A SingleFetch function will return *Instance not found if the key has no corresponding record.

In some cases, this can be a serious error - but in many cases, this is a perfectly acceptable situation.

The easy way to check for this is as follows:

 

Name: Function: MyFunction, Environment<*Object>, Scoped

Call MyFunction

Case

    When Environment<*Call status> != <*Call status.*Normal>

        Go Sub Catch   

    When Environment<*Returned status> == <*Returned status.*Successful>

 

    When Environment<*Returned status> == <*Returned status.*Instance not found>

 

    Otherwise

        Go Sub Catch   

 

In most cases, you would also specify processing for the successful and the instance not found return codes - but this is irrelevant in this case.

By entering this code, you ensure that any abnormal call status or any unexpected returned status is reported in the message log.

Handling specific errors from a function

In most Plex models we have seen, the function just return one of the standard values of the *Returned Status field. The normal usage is that if no error has occurred, the status is set to *Successful; if an error has occurred, the status is set to *Error; if a function has tried to read a non-existing record, the status will be set to *Instance not found.

In many situations, this is all you need. However, all errors are not necessarily created equal - so in some situations, you might want to report a more specific error to the calling function - and maybe also make the administrator aware of the problem.

If the called function encounters an error it can't handle itself, it can report this to the message log by writing a message that describes the error to the message log.

We recommend that you create a special function inheriting from Abstract.CreateLogMessage for each error you want to be able to report to the log.

Writing the message to the message log ensures that the administrator is made aware that the error has happened - this can also be very helpful when you develop your applications.

The other thing you might want to do, is to inform the calling function of the exact type of error that has occurred.

The CreateLogMessage returns a message id as an output field. This identifies the created message record in the message log.

This field inherits from *Returned Status - so you can assign the message id to the Environment<*Returning Status> field.

After this, terminate the function.

 

The calling function can use this message id in a number of ways:

  1. Show a generic error text to the user; include the message id in the text.
  2. Read the message from the message log; show the text to the user
  3. Find the message type of the returned message - perform specific actions for each message type.

 

Examples

The EventHandler HandleOrdersForCustomer calls the service function FindOrdersForCustomer.

The FindOrdersForCustomer has a customer id as an input field and it starts by reading some information for this customer.

If customer does not exist, the FindOrdersForCustomer function must be able to reports this.

To be able to report this, the developer of the FindOrdersForCustomer function creates a CreateLogMessage function.

Source Object Verb Target Object
CustomerDoesNotExist is a FNC Abstract.CreateLogMessage
CustomerDoesNotExist implement SYS Yes
CustomerDoesNotExist input field FLD CustomerID
CustomerDoesNotExist option NME WSYAPI/Error

Enter the following text in the CustomerDoesNotExist.MessageText source code:

    The specified customer &(1:) does not exist.

The option triple determines the severity of the message in the message log. If you do not specify an option, the severity will be "Informative".

Add the following code to throw the exception in the FindOrdersForCustomer function:

 

Name: Function: Customer.Fetch.SingleFetch, Environment<*Object>, Scoped

Call Customer.Fetch.SingleFetch

Case

    When Environment<*Call status> != <*Call status.*Normal>

        Go Sub Catch   

    When Environment<*Returned status> == <*Returned status.*Successful>

 

    When Environment<*Returned status> == <*Returned status.*Instance not found>

        Name: Function: CustomerDoesNotExist, Environment<*Object>, Scoped

        Call CustomerDoesNotExist

        Go Sub Catch

        Environment<*Returning status> = CustomerDoesNotExist/Output<APIFields.MessageID>

        Go Sub Terminate

    Otherwise

        Go Sub Catch   

 

Example 1: Showing a generic error message to the user

This would for example be relevant if HandleOrdersForCustomer had just read the customer that it had specified as input to FindOrdersForCustomer.

In this case, it would be a most unexpected situation that FindOrdersForCustomer did not find the customer - but not completely impossible.

In this case, you must inform the user that his processing has failed - and you might want to get the user to contact the system administrator to start the identification and handling of the error.

Create the following message:

Source Object Verb Target Object
HandleOrdersForCustomer Message MSG UnexpectedError
HandleOrdersForCustomer.UnexpectedError Parameter FLD WSYAPI/APIFields.MessageID

With the text:

An unexpected error occurred. Please contact the administrator. Error code &(1:).

 

Name: Function: FindOrdersForCustomer, Environment<*Object>, Scoped

Call FindOrdersForCustomer

If Environment<*Call status> != <*Call status.*Normal>

    Go Sub Catch

If Environment<*Returned Status> != <*Returned Status *Successful>

    Format Message Message: HandleOrdersForCustomer.UnexpectedError, Environment<*Message Text>

Map with Environment<*Returned Status>

    Go Sub Send message

    Go Sub ErrorHandler

This way, you inform the user that his action has failed - and if the user contacts the administrator, the administrator will be able to find the exact message in the message log.

The user has been informed that an error occurred butt has not been informed which kind of error (which will often be desirable for external users).

 

Example 2: Showing the message log message to the user

This could be relevant if the user has entered the user id - or maybe if the user is the person who can investigate why the customer does not exist.

 

Name: Function: FindOrdersForCustomer, Environment<*Object>, Scoped

Call FindOrdersForCustomer

If Environment<*Call status> != <*Call status.*Normal>

    Go Sub Catch

If Environment<*Returned Status> != <*Returned Status *Successful>

    APIServer.Messages.GetTextForMessage

Map with:

Environment<*Returned Status>

Language<APIFields.LanguageCode>

APIServer.Session.GetBasicSessionData/Output<APIFields.SiteKey>

    Cast Environment<*Message Text>, APIServer.Messages.GetTextForMessage

    Go Sub Send message

    Go Sub ErrorHandler

In this way, you show the log message to the end user (possibly translated to the language of the user) - and he can react.

 

Example 3: Resolving the message type - and handling the error depending on the type

You can do this in two ways. You can either use the APIServer.Messages.GetMessageType function to retrieve the message type and code a case structure that can handle the different errors, or you can use the Catch subroutine to resolve the message type.

As the use of the GetMessageType API is quite simple, this describes the second approach.

If you just call the Catch subroutine after the FindOrdersForCustomer function, any message id will be reported and handled as an unexpected error (see the basic use of the Catch subroutine above.

However, you can override this behavior - and at the same time instruct the Catch subroutine to resolve the message type by adding the field "e" scoped by the implemented CreateLogMessage function.

Source Object Verb Target Object
HandleOrdersForCustomer local FLD

...for VAR

CustomerDoesNotExist.e

WSYAPI/Catch

 

Name: Function: FindOrdersForCustomer, Environment<*Object>, Scoped

Call FindOrdersForCustomer

Go Sub Catch

If Environment<*ReturnedEventType> == Catch<CustomerDoesNotExist.e>

    * Enter handling of the error here

   

The end-result of this is:

When the FindOrdersForCustomer finds that the customer does not exist, it will return the message id of a message of the type CustomerDoesNotExist in the status field.

In the HandleOrdersForCustomer, the Catch subroutine finds the message type of the returned message and compares it with message types in the Catch variable. As the message type is found, the Catch subroutine populates the field Environment<*ReturnedEventType> with the message type id of the message.

Now, the developer can enter his own error handling for this specific event - or he can choose to ignore it completely.

If FindOrdersForCustomer is returning other message types, HandleOrdersForCustomer will report this in the log, and the developer can start handling these errors.

One thing you need to notice is that when you add the "e" field of an event to the Catch variable, you take responsibility for handling this message type everywhere in the calling function.